package com.fiona.remoting.api;

import java.net.InetSocketAddress;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fiona.common.exception.RemotingException;

import cn.hutool.core.net.NetUtil;

public abstract class AbstractClient implements Client {

	private Logger logger = LoggerFactory.getLogger(getClass());
	private final Lock connectLock = new ReentrantLock();
	private final boolean needReconnect = false;
	private InetSocketAddress remoteAddress;
	private volatile boolean closed;

	public AbstractClient(InetSocketAddress address) throws RemotingException {
		this.remoteAddress = address;
		try {
		doOpen();
		} catch (Throwable t) {
			close();
			throw new RemotingException("Failed to start " + getClass().getSimpleName() + " connect to the server "
					+ remoteAddress.toString() + ", cause: " + t.getMessage(), t);
		}

		try {
			// connect.
			connect();
			if (logger.isInfoEnabled()) {
				logger.info(
						"Start " + getClass().getSimpleName() + " connect to the server " + remoteAddress.toString());
			}
		} catch (RemotingException t) {
			close();
			throw t;
		} catch (Throwable t) {
			close();
			throw new RemotingException("Failed to start " + getClass().getSimpleName() + " connect to the server "
					+ remoteAddress.toString() + ", cause: " + t.getMessage(), t);

		}
	}

	public InetSocketAddress getConnectAddress() {
		return remoteAddress;
	}

	@Override
	public boolean isConnected() {
		Channel channel = getChannel();
		if (channel == null) {
			return false;
		}
		return channel.isConnected();
	}

	@Override
	public Object send(Object message) throws RemotingException {
		if (needReconnect && !isConnected()) {
			connect();
		}
		Channel channel = getChannel();
		// TODO Can the value returned by getChannel() be null? need improvement.
		if (channel == null || !channel.isConnected()) {
			throw new RemotingException(
					"message can not send, because channel is closed . url:" + remoteAddress.toString());
		}
		return channel.send(message);
	}

	public void connect() throws RemotingException {
		connectLock.lock();

		try {
			if (isConnected()) {
				return;
			}

			if (isClosed()) {
				logger.warn("No need to connect to server " + remoteAddress.toString() + " from "
						+ getClass().getSimpleName() + " " + NetUtil.getLocalhost()
						+ " , cause: client status is closed or closing.");
				return;
			}

			doConnect(remoteAddress);

			if (!isConnected()) {
				throw new RemotingException("Failed connect to server " + remoteAddress.toString() + " from "
						+ getClass().getSimpleName() + " " + NetUtil.getLocalhost() + ", cause: Connect wait timeout:  ms.");

			} else {
				if (logger.isInfoEnabled()) {
					logger.info("Successed connect to server " + remoteAddress.toString() + " from "
							+ getClass().getSimpleName() + " " + NetUtil.getLocalhost() + ", channel is "
							+ this.getChannel());
				}
			}

		} catch (RemotingException e) {
			throw e;

		} catch (Throwable e) {
			throw new RemotingException("Failed connect to server " + remoteAddress.toString() + " from "
					+ getClass().getSimpleName() + " " + NetUtil.getLocalhost() + " , cause: " + e.getMessage(), e);

		} finally {
			connectLock.unlock();
		}
	}

	public void disconnect() {
		connectLock.lock();
		try {
			try {
				Channel channel = getChannel();
				if (channel != null) {
					channel.close();
				}
			} catch (Throwable e) {
				logger.warn(e.getMessage(), e);
			}
			try {
				doDisConnect();
				closed = true;
			} catch (Throwable e) {
				logger.warn(e.getMessage(), e);
			}
		} finally {
			connectLock.unlock();
		}
	}

	@Override
	public void reconnect() throws RemotingException {
		if (!isConnected()) {
			connectLock.lock();
			try {
				if (!isConnected()) {
					disconnect();
					connect();
				}
			} finally {
				connectLock.unlock();
			}
		}
	}

	@Override
	public void close() {
		if (isClosed()) {
			logger.warn("No need to close connection to server " + remoteAddress.toString() + " from "
					+ getClass().getSimpleName() + " " + NetUtil.getLocalhost()
					+ "  cause: the client status is closed.");
			return;
		}

		connectLock.lock();
		try {
			if (isClosed()) {
				logger.warn("No need to close connection to server " + remoteAddress.toString() + " from "
						+ getClass().getSimpleName() + " " + NetUtil.getLocalhost()
						+ "  cause: the client status is closed.");
				return;
			}

			try {
				disconnect();
			} catch (Throwable e) {
				logger.warn(e.getMessage(), e);
			}

			try {
				doClose();
			} catch (Throwable e) {
				logger.warn(e.getMessage(), e);
			}

		} finally {
			connectLock.unlock();
		}
	}
	@Override
	public boolean isClosed() {
		return closed;
	}
	@Override
	public String toString() {
		return getClass().getName() + " [" + NetUtil.getLocalhost() + " -> " + remoteAddress.toString() + "]";
	}

	/**
	 * Open client.
	 *
	 * @throws Throwable
	 */
	protected abstract void doOpen() throws Throwable;

	/**
	 * Close client.
	 *
	 * @throws Throwable
	 */
	protected abstract void doClose() throws Throwable;

	/**
	 * Connect to server.
	 *
	 * @throws Throwable
	 */
	protected abstract void doConnect(InetSocketAddress inetSocketAddress) throws Throwable;

	/**
	 * disConnect to server.
	 *
	 * @throws Throwable
	 */
	protected abstract void doDisConnect() throws Throwable;

	/**
	 * Get the connected channel.
	 *
	 * @return channel
	 */
	protected abstract Channel getChannel();

}
